<!--最短路径呈现内容-->
<template>
  <div class="theShortestPath">
    <div class="content">
      <div class="content-checkRelation">
        <div class="checkRelation-icon">
          <div class="icon nameFrom"></div>
          <div class="longString"></div>
          <div class="icon nameTo"></div>
        </div>
        <div class="checkRelation-companyName">
          <div class="companyName-name" :class="{ promptWord: !nameFrom }">
            <div class="name" @click="showPopup('nameFrom')">{{ nameFrom ? nameFrom : "请输入第一个公司" }}</div>
            <img src="@/assets/images/close.png" alt="关闭" v-if="nameFrom" @click="nameFrom = ''" />
          </div>
          <div class="companyName-name" :class="{ promptWord: !nameTo }">
            <div class="name" @click="showPopup('nameTo')">{{ nameTo ? nameTo : "请输入第二个公司" }}</div>
            <img src="@/assets/images/close.png" alt="关闭" v-if="nameTo" @click="nameTo = ''" />
          </div>
        </div>
        <div
          :class="[nameFrom && nameTo ? 'checkRelation-search' : 'checkRelation-default']"
          class="checkRelation"
          @click="checkRelation"
          >查关系</div
        >
      </div>
      <div class="content-bg" v-loading="loading" element-loading-text="拼命加载中">
        <div class="bg-schematicDrawing" v-show="schematicDisplayStatus">
          <div> <span></span>关系示意<span></span> </div>
          <img src="@/assets/images/shiyitu.png" alt="" v-show="!noData" />
        </div>
        <div class="content-chart" v-show="noData && !loading">
          <div class="chart-path" v-show="radioList.length" @click="radioSelect">
            <div
              class="path-item"
              data-name="p_0"
              data-index="p_0"
              :class="[radio === 'p_0' ? 'path-item-active' : 'path-item-default']"
              >全部路径</div
            >
            <div
              class="path-item"
              :data-name="item"
              :data-index="item"
              v-for="(item, index) in radioList"
              :key="index"
              :class="[radio === item ? 'path-item-active' : 'path-item-default']"
              >路径{{ index + 1 }}</div
            >
          </div>
          <div class="chart-fullScreen" @click="fullScreen" v-show="!fullScreenMode">全屏</div>
          <div class="chart-sampleGraph" v-show="radioList.length && !sampleGraphShow && !fullScreenMode" @click="showSampleGraph"
            >颜色关系示意</div
          >
          <div class="chart-relationSchema" id="chartDom" ref="chartDom"></div>
        </div>
        <div class="noData" v-show="!schematicDisplayStatus && !noData && !loading">
          <div class="bgImg"></div>
          <div class="title">暂无数据...</div>
        </div>
      </div>
      <van-popup v-model="show" position="bottom" :style="{ height: '100%' }" get-container="body">
        <Search @close="show = false" :type="type" @select="select" ref="searchCompany"></Search>
      </van-popup>
      <van-popup v-model="sampleGraphShow" closeable position="bottom" round :style="{ height: '45%' }" class="sampleGraph">
        <div class="header">颜色示意及关系显示</div>
        <div class="colorScheme crud">
          <div class="title">颜色示意</div>
          <div class="colorLine">
            <div class="item" v-for="(item, index) in chartColor" :key="index">
              <div class="icon" :class="item.name === '公司' ? 'company' : 'naturalPerson'"></div>
              <div class="name">{{ item.name }}</div>
            </div>
          </div>
        </div>
        <div class="relationalDisplay crud">
          <div class="title">关系显示</div>
          <div class="relationLine">
            <div class="item" v-for="item in relationshipLineOption" :key="item">
              <div class="icon" :style="{ background: relationConfig[item].color }"></div>
              <div class="name"> {{ relationConfig[item].name }}</div>
            </div>
          </div>
        </div>
      </van-popup>
    </div>
    <!--全屏图表-->
    <div class="fullScreen-chart-relationSchema" v-if="fullScreenMode">
      <div class="relationSchema-exitFullScreen" @click="exitFullScreen">退出全屏</div>
      <div class="exitFullScreen-relationSchema" id="chartDomRelationSchema"></div>
    </div>
  </div>
</template>
<script>
import { getCheckRelation } from "@/api";
import * as _ from "lodash";
import { relationConfig } from "@/utils/relationConfig";
import Search from "./search.vue";
import * as echarts from "echarts";
const DEFAULT_FONT_SIZE = 6; //默认字体大小
// 公司之间关系类型
const COMPANY_RELATIONS = 'OWN,SERVE_ALL,INVEST,BRANCH';
// 关系查询深度
const COMPANY_RELATIONS_DEPTH = 5;
// 关系图节点默认尺寸
const DEFAULT_NODE_SIZE = 40
// 节点和文字缩放比例
const FONT_NODE_ZOOM_SIZE = 0.5
// 原始比例
const DEFAULT_SCALE_RATIO = 1
// 页面像素转rem的缩放比
const PX_TO_REM_SCALE = 0.5
// 需要显示路径数值的类型 INVEST - 参股类型
const SHOW_PATH_VALUE_TYPES = ["INVEST"];
export default {
  name: "PresentContent",
  components: { Search },
  data() {
    return {
      show: false, //弹出层状态
      sampleGraphShow: false, //颜色关系示意弹窗状态
      schematicDisplayStatus: true, //示意图显示状态
      nameFrom: this.$route.query.companyName, //查询公司名
      nameTo: "", //目标公司名
      type: "", //搜索类型
      loading: false, //加载状态loading
      noData: false, //无数据状态
      radio: "p_0", //默认选中全部路径
      radioList: [], //路径选择组
      fullScreenMode: false, //全屏状态
      relativeColor: {
        enterpriseInvestment: "#F25A29", //企业投资
        personalInvestment: "#ffd209", //个人投资
        tenureRelationship: "#80c2d8", //任职关系
        legalPersonRelationship: "#cce198", //法人关系
      }, //关系颜色
      chartData: {}, //图表数据
      currentChartData: {}, //备份当前图表数据
      atPresentChartData: {}, //当前图表数据
      chartColor: [], //图列颜色
      relationLine: [], //关系线
      relationshipLineOption: [], //关系线选项
      relationConfig, //关系配置
      myChart: null, //图表
      myChartDom: null, //图表dom
      fullScreenMyChart: null, //全屏图表实例
      fullScreenMyChartDom: null, //全屏图表dom
      fontSize: DEFAULT_FONT_SIZE, //初始化字体大小
    };
  },
  methods: {
    /**
     * 退出全屏
     */
    exitFullScreen() {
      this.fullScreenMode = !this.fullScreenMode;
    },
    /**
     * 全屏关系图
     */
    fullScreen() {
      this.fullScreenMode = !this.fullScreenMode;
      this.$nextTick(() => {
        this.fullScreenMyChartDom = document.getElementById("chartDomRelationSchema");
        this.chartDom(this.fullScreenMyChart, this.fullScreenMyChartDom);
      });
    },
    /**
     * 显示颜色关系示意
     */
    showSampleGraph() {
      this.sampleGraphShow = true;
    },
    /**
     * 选择路径
     * @param e
     */
    radioSelect(e) {
      const value = e.target.dataset.index;
      if (!value) {
        //当没有选择路径
        return;
      }
      this.radio = value;
      this.atPresentChartData = this.chartData[value]; //当前图表数据
      this.currentChartData = _.cloneDeep(this.atPresentChartData);
      this.setChartColor(this.atPresentChartData); //设置图列颜色
      this.getRelationLine(this.atPresentChartData); //获取关系线
      this.fontSize = DEFAULT_FONT_SIZE;
      this.chartDom(this.myChart, this.myChartDom);
    },
    /**
     * 获取关系描述
     * 该函数作用主要判断当前关系是否需要显示具体数值，例如参股类型显示具体入股比例
     * @param {string} relationType - 关系类型
     * @param {Array} labels - 关系类型描述（天眼查以数组类型返回）
     * @param {string} percent - 关系数值（天眼查返回的数值是字符串）
     */
    getRelationDescribe(relationType, { labels = [], percent = '0' }) {
      // 合并描述内容
      const describe = labels.join("");
      // 若显示数值，返回描述文字 + 百分比数值（已确认，若数值是 0 也需要显示）
      if (SHOW_PATH_VALUE_TYPES.includes(relationType)) {
        return `${describe} ${percent * 100}%`;
      }
      // 否则，直接返回描述文字
      return describe;
    },
    /**
     * 对给定的节点和关系数据进行处理，用于生成图表的布局。
     * @param {Array} nodes - 节点数组。
     * @param {Array} relationships - 关系数组。
     * @param {Object} coordinates - 全部节点坐标集合。
     * @returns {Object} - 返回一个包含nodes和relationships的对象，用于图表渲染。
     */
    chartArrangement(nodes, relationships, coordinates) {
      const mergedLabels = relationships.reduce((acc, { startNode, endNode, properties, type }) => {
        // 获取关系描述
        const describeValue = this.getRelationDescribe(type, properties);
        // 构建键，表示起始节点和结束节点的组合
        const key = `${startNode},${endNode}`;
        // 如果键已经在累加器对象中，则更新值
        if (key in acc) {
          // 将当前项的labels与已有labels合并，用"\n"分隔
          acc[key].value += `\n${describeValue}`;
        } else {
          // 如果键不存在，则添加新项到累加器对象
          acc[key] = { type, startNode, endNode, value: describeValue };
        }
        // 返回更新后的累加器对象
        return acc;
      }, {});
      //关系线数据整理
      const relationshipsData = Object.values(mergedLabels).map((item) => {
        const source = this.getNodeName(nodes, item.startNode);
        const target = this.getNodeName(nodes, item.endNode);
        return {
          type: item.type,
          source,
          target,
          value: item.value,
          lineStyle: {
            color: relationConfig[item.type].color,
          },
        };
      });
      //处理nodes数据，将其分类为公司或自然人，并准备其在图表中的展示信息。
      const nodesData = nodes.map((item) => {
        const { labels, properties, id } = item
        //labels里面只有一个数据，所以取第一个
        const category = labels[0] === "Company" ? "公司" : "自然人";

        // 获取节点尺寸及坐标信息
        const coordinateData = coordinates[id] || {};
        // 计算节点尺寸：eChart中的节点尺寸是个固定值，不会自适应缩放，因此需要手动调整。在移动端展示时，根据页面的缩放比例调整节点尺寸。
        const nodeSize = (coordinateData?.r ?? DEFAULT_NODE_SIZE) * PX_TO_REM_SCALE

        return {
          category,
          ...coordinateData,
          symbolSize: nodeSize,
          name: properties.name,
          draggable: false,
        };
      });
      return {
        nodes: nodesData,
        relationships: relationshipsData,
      };
    },
    /**
     * 根据提供的节点ID，在节点数组中查找对应的节点，并返回该节点的名称。
     * @param {Array} nodes - 节点数组，每个节点对象应包含id和properties属性，其中properties属性应包含name属性。
     * @param {String} id - 需要查找的节点ID。
     * @returns {String|undefined} 如果找到对应的节点，则返回该节点的名称；如果未找到，则返回undefined。
     */
    getNodeName(nodes, id) {
      // 在节点数组中查找具有匹配ID的节点
      const data = nodes.find((value) => id === value.id);
      // 返回找到的节点的名称，如果未找到则返回undefined
      return data?.properties?.name;
    },
    /**
     * 图表数据整理
     * 由于接口提供的字段均是同级数据，包含关系路径、节点坐标、路径样式等等
     * 1、获取关系数据，接口数据中，字段以 P_ 开头的字段是关系路径数据
     * 2、处理关系数据，把节点对应坐标、关系数据，组合
     * @param resData 接口返回原始值
     */
    chartDataCollation(resData) {
      // 获取节点坐标数据
      const { coordinate = {} } = resData
      // 处理节点数据
      this.chartData = Object.keys(resData)
        .filter((key) => key.startsWith("p_"))
        .reduce((acc, key) => {
          const { nodes, relationships } = resData[key];
          acc[key] = this.chartArrangement(nodes, relationships, coordinate);
          return acc;
        }, {});
    },
    /**
     * 查关系
     */
    async checkRelation() {
      if (!this.nameFrom || !this.nameTo) {
        return;
      }
      this.loading = true;
      const { data } = await getCheckRelation({
        nameTo: this.nameTo,
        nameFrom: this.nameFrom,
        depth: COMPANY_RELATIONS_DEPTH,
        types: COMPANY_RELATIONS,
      });
      
      this.schematicDisplayStatus = false;
      if (!data || data.isRelationship.includes("不存在关联关系")) {
        //当data为空或者data.isRelationship不存在关联关系
        this.loading = false;
        this.noData = false;
        this.myChart?.dispose();
        return;
      }
      this.loading = false;
      this.noData = true;
      this.chartDataCollation(data); //整理图表数据
      this.radio = "p_0";
      this.atPresentChartData = this.chartData[this.radio]; //当前图表数据
      this.currentChartData = _.cloneDeep(this.atPresentChartData);
      this.setChartColor(this.atPresentChartData); //设置图列颜色
      this.getRelationLine(this.atPresentChartData); //获取关系线
      this.radioList = Object.keys(data).filter((key) => key.startsWith("p_") && key !== "p_0");
      this.fontSize = DEFAULT_FONT_SIZE;
      this.$nextTick(() => {
        this.myChart?.dispose();
        this.chartDom(this.myChart, this.myChartDom);
      });
    },
    /**
     * 渲染图表
     * @param instance  图表实例
     * @param dom  图表dom
     */
    chartDom(instance, dom) {
      instance = echarts.init(dom);
      window.onresize = () => {
        if (instance) instance.resize();
      };
      const options = {
        animationDurationUpdate: 1500,
        animationEasingUpdate: "quinticInOut",
        series: [
          {
            type: "graph",
            layout: "none",
            focusNodeAdjacency: true,
            roam: true,
            zoom: 1,
            nodeScaleRatio: FONT_NODE_ZOOM_SIZE, // 节点自身缩放因子
            label: {
              overflow: "break",
              show: true,
              textStyle: {
                fontSize: this.fontSize,
                color: "#fff",
              },
              formatter: function (params) {
                // 使用正则表达式一次性将params.name按每5个字符分割，然后\n换行，去除收尾空格
                return params.name
                  .match(/.{1,5}/g)
                  .join("\n")
                  .trimEnd();
              },
            },
            force: {
              friction: 0.1,
              repulsion: 15000, //关系线长度
            },
            scaleLimit: {
              min: 0.8,
              max: 6
            },
            edgeSymbol: ["circle", "arrow"],
            edgeSymbolSize: [4, 10],
            edgeLabel: {
              show: true,
              textStyle: {
                fontSize: this.fontSize,
              },
              formatter: "{c}",
            },
            categories: this.chartColor,
            data: this.atPresentChartData.nodes,
            links: this.atPresentChartData.relationships,
            lineStyle: {
              type: "dotted",
              opacity: 0.9,
              width: 2,
              curveness: 0,
              emphasis: {
                // 高亮状态
                type: "solid",
              },
            },
          },
        ],
      };
      options && instance.setOption(options);
      //设置文字(缩放文字)
      const that = this;
      const newMyChart = instance;
      let timeout;
      instance.getZr().on("mousewheel", function () {
        if (timeout) {
          clearTimeout(timeout);
        }
        timeout = setTimeout(() => {
          const option = newMyChart.getOption(); //获取图表配置
          const series = option.series[0]; // 提取第一项配置项，因为只有一项配置。且避免重复访问
          const zoom = series.zoom; //获取缩放比列
          // 计算文字缩放尺寸
          that.fontSize = that.zoomFontSize(zoom);
          series.label.textStyle.fontSize = that.fontSize;
          series.edgeLabel.textStyle.fontSize = that.fontSize;
          newMyChart.setOption(option);
          timeout = null; // 在执行完后清空timeout，防止内存泄漏
        }, 100);
      });
    },
    /**
     * 计算节点文字的缩放尺寸
     * 
     * 在 eChart 中，图形缩放不会影响字体大小，需要手动调整。
     * 
     * 计算方式如下：
     * - zoom 代表当前图形的最新缩放级别。
     * - FONT_NODE_ZOOM_SIZE 是节点的缩放因子，用于调整节点在缩放后的大小。
     * 
     * 缩放计算步骤：
     * 1. (zoom - DEFAULT_SCALE_RATIO) 计算相对缩放倍数，表示相对于基础缩放级别的差值。
     * 2. 缩放差值 * FONT_NODE_ZOOM_SIZE 得到基于缩放因子计算实际的缩放值。
     * 3. 缩放差值 * FONT_NODE_ZOOM_SIZE + DEFAULT_SCALE_RATIO 以确保在基础缩放级别 (zoom = 1) 时节点的大小保持不变，得到最终的缩放比。
     * 4. 最终的真实缩放值为：`需要缩放的实际尺寸 * 最终缩放比`
     * 
     * @param zoom 实际缩放比例
     */
    zoomFontSize(zoom) {
      const zoomRatio =  (zoom - DEFAULT_SCALE_RATIO) * FONT_NODE_ZOOM_SIZE + DEFAULT_SCALE_RATIO
      return DEFAULT_FONT_SIZE * zoomRatio;
    },
    /**
     * 选择的公司名
     * @param value
     */
    select(value) {
      this[value.type] = value.name;
      this.show = false;
    },
    /**
     * 展示弹出层
     */
    showPopup(value) {
      this.show = true;
      this.type = value;
      this.$nextTick(() => {
        this.$refs.searchCompany.reset();
      });
    },
    /**
     * 设置图列颜色
     * @param value
     */
    setChartColor(value) {
      this.chartColor = Array.from(new Set(value.nodes.map((item) => item.category))).map((item) => {
        const naturalPerson = item === "自然人";
        return {
          name: item,
          value: naturalPerson ? "Human" : "Company",
          itemStyle: {
            color: naturalPerson ? "#F25A29" : "#0183fe",
          },
        };
      });
    },
    /**
     * 获取关系线
     * @param value
     */
    getRelationLine(value) {
      this.relationLine = Array.from(new Set(value.relationships.map((item) => item.type)));
      this.relationshipLineOption = [...this.relationLine];
    },
    /**
     * 设置关系线
     * @param value
     */
    setRelationLine(value) {
      this.atPresentChartData.relationships = this.currentChartData.relationships.filter((item) => {
        return value.includes(item.type);
      });
      this.chartDom(this.myChart, this.myChartDom);
    },
  },
  mounted() {
    this.myChartDom = document.getElementById("chartDom");
  },
};
</script>
<style lang="less" scoped>
.theShortestPath {
  width: 100%;
  height: 100%;
  .content {
    width: 100%;
    height: 100%;
    padding: 20px;
    box-sizing: border-box;
    background-color: #f4f4f4;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
    .content-checkRelation {
      width: auto;
      height: 160px;
      background: #ffffff;
      border: 1px solid #f5f5f5;
      box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.12);
      border-radius: 16px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 24px;
      .checkRelation-icon {
        width: 16px;
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-evenly;
        padding: 20px 0;
        box-sizing: border-box;
        .icon {
          width: 16px;
          height: 16px;
          border-radius: 50%;
        }
        .nameTo {
          background: #f88642;
        }
        .nameFrom {
          background: #429df8;
        }
        .longString {
          width: 4px;
          height: 24px;
          background: #f5f5f5;
        }
      }
      .checkRelation-companyName {
        .companyName-name {
          width: 478px;
          height: 78px;
          display: flex;
          align-items: center;
          font-size: 28px;
          font-weight: 400;
          justify-content: space-between;
          &:nth-child(1) {
            border-bottom: 2px solid #f5f5f5;
          }
          .name {
            flex: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }
          img {
            width: 64px;
            height: 64px;
            cursor: pointer;
          }
        }
        .promptWord {
          color: #909399;
        }
      }
      .checkRelation {
        width: 128px;
        height: 64px;
        border-radius: 8px;
        font-size: 28px;
        color: #f5ffff;
        font-weight: 500;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
      }
      .checkRelation-default {
        background: #9fcefa;
      }
      .checkRelation-search {
        background: #429df8;
      }
    }
    .content-bg {
      width: 100%;
      height: 100%;
      .bg-schematicDrawing {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin-top: 64px;
        img {
          margin-top: 24px;
          width: 100%;
          object-fit: contain;
        }
      }
      .content-chart {
        width: 100%;
        height: 100%;
        position: relative;
        display: flex;
        flex-direction: column;
        .chart-path {
          display: flex;
          column-gap: 20px;
          row-gap: 20px;
          margin: 19px 0;
          overflow-y: hidden;
          overflow-x: auto;
          -webkit-overflow-scrolling: touch;
          scroll-behavior: smooth;
          &::-webkit-scrollbar {
            display: none;
          }
          .path-item {
            flex-shrink: 0;
            border-radius: 15px;
            font-size: 24px;
            font-weight: 500;
            height: 26px;
            padding: 8px 12px;
            display: flex;
            align-items: center;
            justify-content: center;
          }
          .path-item-active {
            background: #429df8;
            color: #ffffff;
          }
          .path-item-default {
            background: #ffffff;
            color: #606266;
          }
        }
        .chart-fullScreen {
          position: absolute;
          top: 100px;
          right: 20px;
          color: rgb(66, 157, 248);
          z-index: 999;
        }
        .chart-sampleGraph {
          width: 216px;
          height: 96px;
          position: absolute;
          top: 400px;
          left: -20px;
          background: #ffffff;
          box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
          border-radius: 0 48px 48px 0;
          font-size: 28px;
          color: #606266;
          font-weight: 400;
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 999;
        }
        .chart-relationSchema {
          width: 100%;
          height: 100%;
          border: 1px solid #e3e3e3;
        }
      }
      .noData {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        flex-direction: column;
        align-items: center;
        .bgImg {
          width: 300px;
          height: 300px;
          background: url("../../../assets/images/zanwushuju.png") no-repeat center/100% 100%;
        }
      }
    }
    .sampleGraph {
      box-sizing: border-box;
      padding: 32px;
      display: flex;
      flex-direction: column;
      .header {
        font-size: 32px;
        color: #303133;
        text-align: center;
        font-weight: 500;
        margin-bottom: 72px;
      }
      .crud {
        margin-bottom: 40px;
        .title {
          font-size: 28px;
          font-weight: 400;
          color: #909399;
          margin-bottom: 24px;
        }
        .colorLine,
        .relationLine {
          height: 108px;
          display: flex;
          column-gap: 16px;
        }
      }
      .colorScheme {
        .colorLine {
          .item {
            width: 160px;
            height: 108px;
            display: flex;
            flex-direction: column;
            align-items: center;
            row-gap: 16px;
            .icon {
              width: 28px;
              height: 28px;
              border-radius: 50%;
            }
            .company {
              background-color: #0183fe;
            }
            .naturalPerson {
              background-color: #f25a29;
            }
            .name {
              font-size: 24px;
              color: #303133;
              text-align: center;
              font-weight: 500;
            }
          }
        }
      }
      .relationalDisplay {
        .relationLine {
          overflow-y: hidden;
          overflow-x: auto;
          -webkit-overflow-scrolling: touch;
          scroll-behavior: smooth;
          &::-webkit-scrollbar {
            display: none;
          }
          .item {
            flex-shrink: 0;
            width: auto;
            height: 108px;
            padding: 0 32px;
            background: #ecf5fe;
            border-radius: 4px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            row-gap: 16px;
            .icon {
              width: 64px;
              height: 8px;
            }
            .name {
              font-size: 24px;
              text-align: center;
              font-weight: 500;
              color: #429df8;
            }
          }
        }
      }
    }
  }
  .fullScreen-chart-relationSchema {
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    background: #fff;
    box-sizing: border-box;
    z-index: 99;
    .relationSchema-exitFullScreen {
      position: absolute;
      top: 20px;
      right: 20px;
      color: rgb(66, 157, 248);
      z-index: 999;
    }
    .exitFullScreen-relationSchema {
      width: 100%;
      height: 100%;
    }
  }
}
</style>
